﻿using System;
using System.Collections.Generic;
using System.Linq;
using Notoric.Commanding.CommandBase;
using Notoric.ViewModel.Elements.CWNotation;

namespace Notoric.ViewModel
{
    /// <summary>
    /// An undoable command for inserting a new note symbol.
    /// </summary>
    /// <remarks>
    /// Input Parameters:
    /// <list type="table">
    /// <listheader>
    /// <term>
    ///     Name
    /// </term>
    /// <description>
    ///     Description
    /// </description>
    /// </listheader>
    /// <item>
    /// <term>
    ///     <see cref="AugmentationDots"/> (<see cref="Byte"/>)
    /// </term>
    /// <description>
    ///     The number of augmentation dots.
    /// </description>
    /// </item>
    /// <item>
    /// <term>
    ///     <see cref="BasicDuration"/> (<see cref="UInt16"/>)
    /// </term>
    /// <description>
    ///     The basic duration (without augmentation dots) of the inserted symbol.
    /// </description>
    /// </item>
    /// <item>
    /// <term>
    ///     <see cref="Beat"/> (<see cref="UInt16"/>)
    /// </term>
    /// <description>
    ///     The beat at which the symbol is inserted.
    /// </description>
    /// </item>
    /// <item>
    /// <term>
    ///     <see cref="BeatConflictAction"/> (<see cref="BeatConflictActionType"/>)
    /// </term>
    /// <description>
    ///     The action that should be taken if the target beat is already occupied
    ///     by another symbol in the same voice.
    /// </description>
    /// </item>
    /// <item>
    /// <term>
    ///     <see cref="Measure"/> (<see cref="Model.Elements.MeasureElement"/>)
    /// </term>
    /// <description>
    ///     The measure to which the symbol is inserted.
    /// </description>
    /// </item>
    /// <item>
    /// <term>
    ///     <see cref="Project"/> (<see cref="Elements.Project"/>)
    /// </term>
    /// <description>
    ///     The project to which the symbol will be inserted.
    /// </description>
    /// </item>
    /// <item>
    /// <term>
    ///     <see cref="StaffDescriptor"/> (<see cref="Model.Elements.CWNotation.StaffDescriptor"/>)
    /// </term>
    /// <description>
    ///     The descriptor of the staff into which the symbol is inserted.
    /// </description>
    /// </item>
    /// <item>
    /// <term>
    ///     <see cref="Tone"/> (<see cref="Model.Elements.Tone"/>)
    /// </term>
    /// <description>
    ///     The tone represented by the inserted note.
    /// </description>
    /// </item>
    /// <item>
    /// <term>
    ///     <see cref="Voice"/> (<see cref="Byte"/>
    /// </term>
    /// <description>
    ///     The voice into which the symbol is inserted
    /// </description>
    /// </item>
    /// </list>
    /// </remarks>
    public class InsertNoteCommand : UndoableCommandBase
    {
        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="InsertNoteCommand"/> class.
        /// </summary>
        /// <param name="undoStack">The command stack where the command will be put after its execution</param>
        /// <param name="redoStack">The command stack where the command will be put when it has been undone</param>
        public InsertNoteCommand(CommandStack undoStack, CommandStack redoStack)
            : base(undoStack, redoStack)
        {
            AddParameter<byte>("AugmentationDots", ParameterDirection.In);
            AddParameter<ushort>("BasicDuration", ParameterDirection.In);
            AddParameter<ushort>("Beat", ParameterDirection.In);
            AddParameter<BeatConflictActionType>("BeatConflictAction", ParameterDirection.In);
            AddParameter<Model.Elements.MeasureElement>("Measure", ParameterDirection.In);
            AddParameter<Elements.Project>("Project", ParameterDirection.In);
            AddParameter<Model.Elements.CWNotation.StaffDescriptor>("StaffDescriptor", ParameterDirection.In);
            AddParameter<Model.Elements.Tone>("Tone", ParameterDirection.In);
            AddParameter<byte>("Voice", ParameterDirection.In);

            createdNotes = new List<Note>();
            beamSplitCommands = new List<UndoableCommandBase>();
            wasChordCreated = false;
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Processes the section affected by the insertion operation.
        /// </summary>
        /// <param name="section">
        /// <para>
        /// Type: <see cref="Elements.MusicSection"/>
        /// </para>
        /// <para>
        /// The section.
        /// </para>
        /// </param>
        private void ProcessAffectedSection(Elements.MusicSection section)
        {
            var modelSection = section.AdaptedElement as Model.Elements.MusicSectionElement;

            if (!modelSection.Parts.Contains(StaffDescriptor.FindOwner<Model.Elements.PartDescriptorElement>()))
                return;

            var modelInterior = modelSection.Interior as Model.Elements.CWNotation.MusicSectionInterior;
            var modelSystem = modelInterior.Systems.First(s => s.Measures.Contains(Measure));
            var modelStaff = modelSystem.Staves.FirstOrDefault(s => s.Descriptor == StaffDescriptor);

            if (modelStaff == null)
                return;

            var interior = section.Interior as Elements.CWNotation.MusicSectionInterior;
            var system = interior.Systems.First(s => s.AdaptedElement == modelSystem);
            var staff = system.Staves.First(s => s.AdaptedElement == modelStaff);

            var prevSymbol = (from s in staff.Symbols
                              where s is DurationalSymbol && ((DurationalSymbol)s).Voice == Voice &&
                                    ((Model.Elements.CWNotation.DurationalSymbol)s.AdaptedElement).Measure == Measure &&
                                    ((Model.Elements.CWNotation.DurationalSymbol)s.AdaptedElement).Beat.Beginning < Beat
                              select s as DurationalSymbol).LastOrDefault();

            if (prevSymbol != null)
            {
                if (prevSymbol.GroupBeam != null)
                {
                    if (prevSymbol.GroupBeam.BeamedSymbols[prevSymbol.GroupBeam.BeamedSymbols.Count - 1] != prevSymbol)
                    {
                        var symbols = new List<DurationalSymbol>(
                            from s in prevSymbol.GroupBeam.BeamedSymbols
                            select s
                            );
                        var beamSplitCommand = new SplitBeamGroupCommand(UndoStack, RedoStack)
                        {
                            Symbols = symbols,
                            UseStacks = false
                        };
                        beamSplitCommand.Execute(null);
                        beamSplitCommands.Add(beamSplitCommand);
                    }
                }
            }


            var insertedTone = insertionCommand.GetParameterValue<Model.Elements.ToneElement>("CreatedTone");
            var chord = staff.Symbols.First(
                s => s is Chord &&
                    (s.AdaptedElement as Model.Elements.CWNotation.Chord).PresentedElement == insertedTone.FindOwner<Model.Elements.ChordElement>()
                ) as Chord;
            createdNotes.Add(chord.Notes.First(n => ((Model.Elements.CWNotation.Note)n.AdaptedElement).PresentedTone == insertedTone));

            if (section.FindOwner<Elements.Page>() != null)
                Model.Elements.CWNotation.Layout.Layouter.LayoutHorizontally(modelSystem);
            else
                Model.Elements.CWNotation.Layout.Layouter.LayoutHorizontallyNA(modelSystem);
        }

        #endregion

        #region Command Logic

        /// <inheritdoc/>
        protected override void CommandOperation(object parameter)
        {
            var partDescriptor = StaffDescriptor.FindOwner<Model.Elements.PartDescriptorElement>();
            var duration = Model.Utils.Duration.TotalDuration(BasicDuration, AugmentationDots);

            var part = Measure[partDescriptor];
            var beat = (part != null ? part[Beat] : null);
            var symbol = (beat != null ? beat[Voice] : null);

            if (symbol != null && (BeatConflictAction == BeatConflictActionType.AddToChord || BeatConflictAction == BeatConflictActionType.Throw))
            {
                if (symbol is Model.Elements.RestElement)
                    throw new ExecutionException("Cannot insert a note symbol, since the beat is already occupied by a rest.");
                else if (BeatConflictAction == BeatConflictActionType.Throw)
                    throw new ExecutionException("Cannot insert a note symbol, since the beat is already occupied by another symbol.");

                insertionCommand = new AddToneCommand(UndoStack, RedoStack)
                {
                    Chord = symbol as Model.Elements.ChordElement,
                    Tone = Tone,
                    UseStacks = false
                };
            }
            else
            {
                var customData = new Model.Elements.CWNotation.CWSymbolData(Measure.PackagePart as Model.Packaging.CompositionPart, null, StaffDescriptor);

                var chordCommand = new InsertDurationalSymbolCommand<Model.Elements.ChordElement>(UndoStack, RedoStack)
                {
                    Beat = Beat,
                    CustomData = new Model.Elements.CustomSymbolData[] { customData },
                    Duration = duration,
                    Measure = Measure,
                    Part = partDescriptor,
                    Voice = Voice,
                    UseStacks = false
                };

                insertionCommand = new AddToneCommand(UndoStack, RedoStack)
                {
                    Tone = Tone,
                    UseStacks = false
                };

                insertionCommand.BindParameterToCommand("Chord", chordCommand, "CreatedSymbol");
                wasChordCreated = true;
            }

            insertionCommand.Execute(parameter);

            var references = Measure.FindOwner<Model.Elements.CompositionElement>().GetReferencingPages(Measure, Measure);
            foreach (var reference in references)
            {
                var score = Project.Scores.First(s => s.AdaptedPart == reference.Page.Owner);
                var page = score.Pages.First(p => p.AdaptedPart == reference.Page);
                var section = page.Sections.First(s => s.AdaptedElement == reference.Section) as Elements.MusicSection;

                ProcessAffectedSection(section);
            }

            var composition = Project.Compositions.First(c => c.AdaptedElement == Measure.FindOwner<Model.Elements.CompositionElement>());
            ProcessAffectedSection(composition.LinearView.Content);

            Project.IsModified = true;
        }

        /// <inheritdoc/>
        protected override void RedoOperation()
        {
            insertionCommand.Execute(null);

            foreach (var cmd in beamSplitCommands)
                cmd.Execute(null);

            foreach (var note in createdNotes)
            {
                var chord = note.FindOwner<Chord>();
                var modelChord = chord.AdaptedElement as Model.Elements.CWNotation.Chord;

                if (wasChordCreated)
                {
                    var staff = note.FindOwner<Staff>();
                    var modelStaff = staff.AdaptedElement as Model.Elements.CWNotation.Staff;
                    modelStaff.RestoreSymbol(modelChord);
                    var vmChord = staff.Symbols.First(s => s.AdaptedElement == chord.AdaptedElement);
                    staff.Symbols[staff.Symbols.IndexOf(vmChord)] = chord;
                }

                modelChord.RestoreNote(note.AdaptedElement as Model.Elements.CWNotation.Note);
                chord.RestoreNote(note);

                if (note.FindOwner<Elements.Page>() != null)
                    Model.Elements.CWNotation.Layout.Layouter.LayoutHorizontally(note.AdaptedElement.FindOwner<Model.Elements.CWNotation.System>());
                else
                    Model.Elements.CWNotation.Layout.Layouter.LayoutHorizontallyNA(note.AdaptedElement.FindOwner<Model.Elements.CWNotation.System>());
            }

            Project.IsModified = true;
        }

        /// <inheritdoc/>
        protected override void UndoOperation()
        {
            foreach (var cmd in beamSplitCommands)
                cmd.Execute(null);

            insertionCommand.Execute(null);

            foreach (var note in createdNotes)
            {
                var interior = note.FindOwner<Elements.CWNotation.MusicSectionInterior>();
                interior.ClearSelection();
                var system = note.FindOwner<Elements.CWNotation.System>();
                if (system.FindOwner<Elements.Page>() != null)
                    Model.Elements.CWNotation.Layout.Layouter.LayoutHorizontally(system.AdaptedElement as Model.Elements.CWNotation.System);
                else
                    Model.Elements.CWNotation.Layout.Layouter.LayoutHorizontallyNA(system.AdaptedElement as Model.Elements.CWNotation.System);
            }

            Project.IsModified = true;            
        }

        #endregion

        #region Public Properties
        
        /// <summary>
        /// Gets or sets the number of augmentation dots of the inserted note.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Byte"/>
        /// </para>
        /// <para>
        /// The number of augmentation dots of the inserted note.<br/>
        /// This property wraps the "AugmentationDots" parameter.
        /// </para>
        /// </value>
        public byte AugmentationDots
        {
            get { return GetParameterValue<byte>("AugmentationDots"); }
            set { SetParameterValue<byte>("AugmentationDots", value); }
        }

        /// <summary>
        /// Gets or sets the basic duration of the inserted note.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="UInt16"/>
        /// </para>
        /// <para>
        /// The basic duration (without augmentation dots) of the inserted note.
        /// It is given as a multiple of 128-th beats (e.g. Quarter Note = 32 128-th beats).<br/>
        /// This property wraps the "BasicDuration" command parameter.
        /// </para>
        /// </value>
        public ushort BasicDuration
        {
            get { return GetParameterValue<ushort>("BasicDuration"); }
            set { SetParameterValue<ushort>("BasicDuration", value); }
        }

        /// <summary>
        /// Gets or sets the beat at which the note is inserted.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="UInt16"/>
        /// </para>
        /// <para>
        /// The beat at which the note is inserted.
        /// It is given as a multiple of 128-th beats
        /// (e.g. 1st beat is 0, 2nd beat is 32, etc.).<br/>
        /// This property wraps the "Beat" command parameter.
        /// </para>
        /// </value>
        public ushort Beat
        {
            get { return GetParameterValue<ushort>("Beat"); }
            set { SetParameterValue<ushort>("Beat", value); }
        }

        /// <summary>
        /// Gets or sets the action that should be taken on beat conflict.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="BeatConflictActionType"/>
        /// </para>
        /// <para>
        /// The action that should be taken when the target beat already contains
        /// another symbol in the same voice.<br/>
        /// This property wraps the "BeatConflictAction" command parameter.
        /// </para>
        /// </value>
        public BeatConflictActionType BeatConflictAction
        {
            get { return GetParameterValue<BeatConflictActionType>("BeatConflictAction"); }
            set { SetParameterValue<BeatConflictActionType>("BeatConflictAction", value); }
        }

        /// <summary>
        /// Gets or sets the measure to which the note is inserted.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Model.Elements.MeasureElement"/>
        /// </para>
        /// <para>
        /// The measure to which the note is inserted.<br/>
        /// This property wraps the "Measure" command parameter.
        /// </para>
        /// </value>
        public Model.Elements.MeasureElement Measure
        {
            get { return GetParameterValue<Model.Elements.MeasureElement>("Measure"); }
            set { SetParameterValue<Model.Elements.MeasureElement>("Measure", value); }
        }

        /// <summary>
        /// Gets or sets the project to which the note is being inserted.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Elements.Project"/>
        /// </para>
        /// <para>
        /// The project to which the note is being inserted.<br/>
        /// This property wraps the "Project" command parameter.
        /// </para>
        /// </value>
        public Elements.Project Project
        {
            get { return GetParameterValue<Elements.Project>("Project"); }
            set { SetParameterValue<Elements.Project>("Project", value); }
        }

        /// <summary>
        /// Gets or sets the staff descriptor.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Model.Elements.CWNotation.StaffDescriptor"/>
        /// </para>
        /// <para>
        /// The descriptor of the staff to which the note is being inserted.<br/>
        /// This property wraps the "StaffDescriptor" command parameter.
        /// </para>
        /// </value>
        public Model.Elements.CWNotation.StaffDescriptor StaffDescriptor
        {
            get { return GetParameterValue<Model.Elements.CWNotation.StaffDescriptor>("StaffDescriptor"); }
            set { SetParameterValue<Model.Elements.CWNotation.StaffDescriptor>("StaffDescriptor", value); }
        }

        /// <summary>
        /// Gets or sets the tone represented by the inserted note.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Model.Elements.Tone"/>
        /// </para>
        /// <para>
        /// The tone represented by the inserted note.<br/>
        /// This property wraps the "Tone" command parameter.
        /// </para>
        /// </value>
        public Model.Elements.Tone Tone
        {
            get { return GetParameterValue<Model.Elements.Tone>("Tone"); }
            set { SetParameterValue<Model.Elements.Tone>("Tone", value); }
        }

        /// <summary>
        /// Gets or sets the voice into which the note is inserted.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Byte"/>
        /// </para>
        /// <para>
        /// The number of the voice into which the note is inserted.<br/>
        /// This property wraps the "Voice" command parameter.
        /// </para>
        /// </value>
        public byte Voice
        {
            get { return GetParameterValue<byte>("Voice"); }
            set { SetParameterValue<byte>("Voice", value); }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the sub-commands that has split the previous beam groups in the affected sections (if any).
        /// </summary>
        private List<UndoableCommandBase> beamSplitCommands;
        /// <summary>
        /// Holds the list of notes created by this command.
        /// </summary>
        private List<Note> createdNotes;
        /// <summary>
        /// Holds the sub-command that inserts the symbol into the composition data.
        /// </summary>
        private UndoableCommandBase insertionCommand;
        /// <summary>
        /// Holds the value indicating whether a new chord has been created
        /// before inserting the note or not.
        /// </summary>
        private bool wasChordCreated;

        #endregion
    }

    /// <summary>
    /// Describes the action that should be taken when the symbol is inserted
    /// to a beat that already contains another symbol in the same voice.
    /// </summary>
    public enum BeatConflictActionType
    {
        /// <summary>
        /// Execution exception will be thrown.
        /// </summary>
        Throw,
        /// <summary>
        /// The previous symbol will be moved forward to next beat.
        /// </summary>
        Move,
        /// <summary>
        /// This action can only be taken if both symbols (the inserted and previous one) are chords (notes).
        /// In this case the inserted note will be added to the existing chord.
        /// If necessary, its duration will be changed to match the target chord.
        /// </summary>
        AddToChord
    }
}
